package xyz.thoughtset.viewer.modules.ds.jdbc.core.factory;

import lombok.SneakyThrows;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.ParameterMapping;
import org.apache.ibatis.mapping.ParameterMode;
import org.apache.ibatis.mapping.SqlSource;
import org.apache.ibatis.parsing.XNode;
import org.apache.ibatis.parsing.XPathParser;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.reflection.ParamNameResolver;
import org.apache.ibatis.scripting.xmltags.XMLScriptBuilder;
import org.apache.ibatis.session.Configuration;
import org.apache.ibatis.type.JdbcType;
import org.apache.ibatis.type.TypeException;
import org.apache.ibatis.type.TypeHandler;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import xyz.thoughtset.viewer.executor.core.base.AbstractExecutor;
import xyz.thoughtset.viewer.executor.core.entity.QueryBody;
import xyz.thoughtset.viewer.modules.ds.core.factory.ConnectFactory;
import xyz.thoughtset.viewer.modules.ds.jdbc.core.constant.SupportTypeConstant;

import javax.sql.DataSource;
import java.sql.*;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

@Component
public class MybatisSqlQueryExecutor extends AbstractExecutor {
    @Autowired
    protected ConnectFactory connectFactory;

    public MybatisSqlQueryExecutor() {
        super(SupportTypeConstant.SUPPORT_TYPE, true);
    }

    private final Configuration configuration = new Configuration();


    @Override
    protected Object exec(QueryBody queryBody, Map param) {
        return doQuery(queryBody.getQuery(),param, connectFactory.buildConnect(queryBody.getDsId(),DataSource.class));
    }


    @SneakyThrows
    public List doQuery(String sqlText, Object param, DataSource ds) {
        XPathParser parser = new XPathParser(sqlText);
        XNode xNode = parser.evalNode("/select");
        XMLScriptBuilder builder = new XMLScriptBuilder(new Configuration(), xNode, null);
        SqlSource sqlSource = builder.parseScriptNode();
        Object parameterObject = ParamNameResolver.wrapToMapIfCollection(param, null);
        BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
        List list= new ArrayList<>();
        try(
                Connection connection = ds.getConnection();
                PreparedStatement statement = prepareStatement(boundSql,connection,parameterObject);
                ResultSet rs = getFirstResultSet(statement);
        ){
            ResultSetMetaData data = rs.getMetaData();
            int fieldNum = data.getColumnCount();
            List<String> fieldNames = new ArrayList<>(fieldNum);
            for(int i=1;i<=fieldNum;i++){
                fieldNames.add(data.getColumnLabel(i));
            }
            while (rs.next()) {
                Map<String, Object> resultMap = new HashMap<>();
                for(String field : fieldNames){
                    resultMap.put(field,rs.getObject(field));
                }
                list.add(resultMap);
            }
        }catch (Exception e){
            e.printStackTrace();
        }
//        org.apache.ibatis.executor.SimpleExecutor
//        Executor executor = new SimpleExecutor();
//        StatementHandler handler = new PreparedStatementHandler(wrapper, ms, null, null, null,
//                boundSql);
//        DynamicSqlSource sqlSource = new DynamicSqlSource(new Configuration(), rootSqlNode);

        return list;
    }

    private PreparedStatement prepareStatement(BoundSql boundSql,Connection connection,Object parameterObject) throws SQLException {
        PreparedStatement stmt;
        String sql = boundSql.getSql();
        stmt = connection.prepareStatement(sql);
        setParameters(stmt,boundSql,parameterObject);
        stmt.execute();
        return stmt;
    }

    public void setParameters(PreparedStatement ps,BoundSql boundSql,Object parameterObject) {
//        ErrorContext.instance().activity("setting parameters").object(mappedStatement.getParameterMap().getId());
        List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
        if (parameterMappings != null) {
            for (int i = 0; i < parameterMappings.size(); i++) {
                ParameterMapping parameterMapping = parameterMappings.get(i);
                if (parameterMapping.getMode() != ParameterMode.OUT) {
                    Object value = getValue(parameterMapping,boundSql,parameterObject);
                    TypeHandler typeHandler = parameterMapping.getTypeHandler();
                    JdbcType jdbcType = parameterMapping.getJdbcType();
                    if (value == null && jdbcType == null) {
                        jdbcType = JdbcType.OTHER;
                    }
                    try {
                        typeHandler.setParameter(ps, i + 1, value, jdbcType);
                    } catch (TypeException | SQLException e) {
                        throw new TypeException("Could not set parameters for mapping: " + parameterMapping + ". Cause: " + e, e);
                    }
                }
            }
        }
    }

    private Object getValue(ParameterMapping parameterMapping,BoundSql boundSql,Object parameterObject) {
//        if (parameterMapping.hasValue()) {
//            return parameterMapping.getValue();
//        }
        String propertyName = parameterMapping.getProperty();
        if(parameterObject instanceof Map){
            return ((Map<?, ?>) parameterObject).get(propertyName);
        }
        MetaObject metaObject = null;
        if (boundSql.hasAdditionalParameter(propertyName)) { // issue #448 ask first for additional params
            return boundSql.getAdditionalParameter(propertyName);
        } else if (parameterObject == null) {
            return null;
//        } else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
//            return parameterObject;
        } else {
            if (metaObject == null) {
                metaObject = configuration.newMetaObject(parameterObject);
            }
            return metaObject.getValue(propertyName);
        }
    }

    private ResultSet getFirstResultSet(Statement stmt) throws SQLException {
        ResultSet rs = null;
        SQLException e1 = null;

        try {
            rs = stmt.getResultSet();
        } catch (SQLException e) {
            // Oracle throws ORA-17283 for implicit cursor
            e1 = e;
        }

        try {
            while (rs == null) {
                // move forward to get the first resultset in case the driver
                // doesn't return the resultset as the first result (HSQLDB)
                if (stmt.getMoreResults()) {
                    rs = stmt.getResultSet();
                } else if (stmt.getUpdateCount() == -1) {
                    break;
                }
            }
        } catch (SQLException e) {
            throw e1 != null ? e1 : e;
        }

        return rs;
    }

}
